﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Fetches.Fetch.Dto;
using Fetches.Fetch.Utils.KvStore;
using Serilog;

namespace Fetches.Fetch.Utils.FileHosting
{
    /// <summary>
    ///     二进制数据存储器,由Fetcher负责二进制化主要是因为代理
    /// </summary>
    public class FileHost : IDisposable
    {

        public static FileHost Instance { get; private set; }

        private IKvStore _IdStore;
        private IHostFileProvider _provider;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="provider"></param>
        /// <param name="idStore"></param>
        public FileHost(IHostFileProvider provider, IKvStore idStore)
        {
            if (Instance != null)
            {
                throw new Exception("FileHost作为Factory的组件，单例");
            }

            Instance = this;
            _IdStore = idStore;
            _provider = provider;
        }

        /// <summary>
        ///     判断数据是否已存在，免得反复读取二进制数据浪费时间
        /// </summary>
        /// <returns></returns>
        public bool Exists(HostFileItem item)
        {
            return _IdStore.Exists(item.UniqueId);
        }

        /// <summary>
        ///     集中数据存储，由需要存储数据的调用者负责将数据二进制化
        /// </summary>
        /// <param name="item"></param>
        public async Task HostFileAsync(HostFileItem item)
        {
            if (!Exists(item))
            {
                var hash = HashHelper.Sha1(item.Data);
                _IdStore.Put(item.UniqueId, hash);
                await _provider.WriteAsync(hash, item.Data);
            }
        }


        #region HostFile
        /// <summary>
        /// 下载对应资源到本地
        /// </summary>
        /// <param name="hostingTypeList"></param>
        /// <param name="page"></param>
        /// <param name="getResourceFunc">如果该资源有什么需要绑定Fetcher的话传递这个资源获取方法</param>
        /// <returns></returns>
        public async Task<string> LocalizeResource(IList<string> hostingTypeList, ResponsePage page, Func<string, Uri, Task<byte[]>> getResourceFunc = null)
        {
            var document = new HtmlParser().ParseDocument(page.SourceCode);
            var refer = page.Request.Url.OriginalString;
            foreach (var fileTypeStr in hostingTypeList)
            {
                var fileType = FileTypeHelper.Parse(fileTypeStr);
                switch (fileType)
                {
                    case FileType.Text:
                        await HostTextResource(document, refer, getResourceFunc);
                        break;
                    case FileType.Image:
                    case FileType.Audio:
                    case FileType.Video:
                        await HostMultiMedia(document, refer, fileType, getResourceFunc);
                        break;
                    default:
                        break;
                }

            }

            return document.ToHtml();
        }

        private async Task HostTextResource(IHtmlDocument document, string refer, Func<string, Uri, Task<byte[]>> getResourceFunc)
        {
            var cssLinks = document.QuerySelectorAll<IHtmlLinkElement>("link[rel=stylesheet]");
            foreach (var link in cssLinks)
            {
                var item = new HostFileItem
                {
                    Uri = new Uri(link.Href)
                };
                var success = await HostFileIfNotExits(refer, link.Href, item, getResourceFunc);
                if (success)
                {
                    link.Href = item.GetReceiptUrl();
                }

            }

            var jsLinks = document.QuerySelectorAll<IHtmlScriptElement>("script[src]");
            foreach (var link in jsLinks)
            {
                string src = link.Source;
                var referUri = new Uri(refer);
                if (src.StartsWith("//"))
                {
                    src = referUri.Scheme + ":" + src;
                }

                if (src.StartsWith("/"))
                {
                    src = $"{referUri.Scheme}://{referUri.Host}{src}";
                }

                try
                {
                    var item = new HostFileItem
                    {
                        Uri = new Uri(src)
                    };
                    var success = await HostFileIfNotExits(refer, src, item, getResourceFunc);
                    if (success)
                    {
                        link.Source = item.GetReceiptUrl();
                    }
                }
                catch (Exception e)
                {
                    Log.Debug("error localize resource {url} with {e}", src, e);

                }


            }
        }

        private async Task HostMultiMedia(IHtmlDocument document, string refer, FileType fileType, Func<string, Uri, Task<byte[]>> getResourceFunc)
        {
            switch (fileType)
            {
                case FileType.Image:
                    foreach (var element in document.QuerySelectorAll<IHtmlImageElement>("img"))
                    {
                        //TODO　本地化图片资源时存在lazy load或不同清晰度版本，需要引入规则来处理
                        var receiptUrl = await HostFileReturnReceipt(refer, element.Source, getResourceFunc);
                        if (!string.IsNullOrEmpty(receiptUrl))
                        {
                            element.Source = receiptUrl;
                        }
                    }
                    break;
                case FileType.Audio:
                    foreach (var element in document.QuerySelectorAll<IHtmlAudioElement>("audio"))
                    {
                        var receiptUrl = await HostFileReturnReceipt(refer, element.Source, getResourceFunc);
                        if (!string.IsNullOrEmpty(receiptUrl))
                        {
                            element.Source = receiptUrl;
                        }
                    }
                    break;
                case FileType.Video:
                    foreach (var element in document.QuerySelectorAll<IHtmlVideoElement>("video"))
                    {
                        var receiptUrl = await HostFileReturnReceipt(refer, element.Source, getResourceFunc);
                        if (!string.IsNullOrEmpty(receiptUrl))
                        {
                            element.Source = receiptUrl;
                        }
                    }
                    break;
            }


        }

        private async Task<string> HostFileReturnReceipt(string refer, string src, Func<string, Uri, Task<byte[]>> getResourceFunc)
        {
            if (!string.IsNullOrEmpty(src))
            {
                var item = new HostFileItem
                {
                    Uri = new Uri(src)
                };
                var success = await HostFileIfNotExits(refer, src, item, getResourceFunc);
                if (success)
                {
                    return item.GetReceiptUrl();
                }
            }

            return string.Empty;
        }

        private async Task<bool> HostFileIfNotExits(string refer, string src, HostFileItem item, Func<string, Uri, Task<byte[]>> getResourceFunc)
        {
            try
            {
                if (item.Uri.Scheme != "http" && item.Uri.Scheme != "https")
                {
                    return false;
                }
                if (!Exists(item))
                {
                    Log.Debug("localize resource {src}", src);
                    if (getResourceFunc == null)
                    {
                        var wc = new WebClient
                        {
                            Headers = {["Referer"] = refer}
                        };
                        item.Data = await wc.DownloadDataTaskAsync(src);
                    }
                    else
                    {
                        item.Data = await getResourceFunc(refer, new Uri(src));
                    }

                    await HostFileAsync(item);
                }

                return true;
            }
            catch (Exception e)
            {
                Log.Debug("HostFile error {error}", e);
                return false;
            }

        }

        #endregion

        public void Dispose()
        {
            //_provider.Dispose();
            _IdStore.Dispose();
        }
    }
}